KVC Key Value Coding
KVC
是一种用间接方式访问类的属性的机制。比如你要给一个类中的属性赋值或者取值,可以直接通过类和点运算符实现,当然也可以使用KVC
。不过对于私有属性,点运算符就不起作用,因为私有属性不暴露给调用者,不过使用KVC
却依然可以实现对私有属性的读写。
先看一下KVC
的一部分源码,当然只能看到头文件:
// NSKeyValueCoding.h
@interface NSObject(NSKeyValueCoding)
+ (BOOL)accessInstanceVariablesDirectly;
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath NS_AVAILABLE(10_7, 5_0);
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
@end
@interface NSArray(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
@end
@interface NSDictionary(NSKeyValueCoding)
- (nullable ObjectType)valueForKey:(NSString *)key;
@end
@interface NSMutableDictionary(NSKeyValueCoding)
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;
@end
@interface NSOrderedSet(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);
- (void)setValue:(nullable id)value forKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);
@end
@interface NSSet(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
@end
可以看到这个类里面包含了对类NSObject
,NSArray
,NSDictionary
,NSMutableDictionary
,NSOrderedSet
,NSSet
的拓展。拓展的方法基本上为
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
也就是说,基本上Objective-C里所有的对象都支持KVC
操作,操作包含如上两类方法,动态读取和动态设值。
好多地方说是NSObject实现了NSKeyValueCoding协议。而代码里是类的拓展。这两种说法是相通的嘛?
举个🌰,新建一个Command line程序:
// Account.h
@interface Account : NSObject
@property (nonatomic, assign) float balance;
@end
// Account.m
@implementation Account {
float salaryPerDay;
}
@synthesize balance = _balance;
- (void)setBalance:(float)balance {
NSLog(@"set balance invoked");
_balance = balance;
}
- (float)balance {
NSLog(@"get balance invoked");
return _balance;
}
@end
// Person.h
@class Account;
@interface Person : NSObject {
@private
int _age;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) Account *account;
- (void)showMessage;
@end
// Person.m
#import "Person.h"
@implementation Person {
NSString *_sex;
}
- (void)showMessage {
NSLog(@"name = %@, age = %d, sex = %@", _name, _age, _sex);
}
@end
// main
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person1 = [[Person alloc] init];
[person1 setValue:@"Wossoneri" forKey:@"name"];
[person1 setValue:@25 forKey:@"age"]; //私有变量也可以访问
[person1 setValue:@"male" forKey:@"sex"]; //私有变量也可以访问
[person1 showMessage];
Account *account1 = [[Account alloc] init];
person1.account = account1;
[person1 setValue:@1000.0 forKeyPath:@"account.balance"];
[person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];
NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);
}
return 0;
}
// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
Person1`s balance is : 1000.00
Person1`s salary is : 300.00
代码说明:
- Person类里用旧方法声明私有变量
_age
以及直接添加的私有成员变量_sex
,同时声明一个开放的属性_name
。 - 对于
_name
,O-C会直接为其生成对应的setter
和getter
,所以可以通过点运算符操作属性,比如person1.name = @"Wossoneri";
- 可以看到
KVC
可以对私有变量进行操作。对于当前类的直接成员变量,把变量名作为key来访问,否则要写成keyPath来访问。 KVC
运行时首先会优先调用属性的getter
和setter
,这一点可以在代码输出的第二行和第三行看到,如果没有,就会优先搜索_property
,不存在则搜索property
,如果仍然没有,就会调用setValue:forUndefinedKey:
和valueForUndefinedKey:
方法
KVO Key Value Observing
介绍
KVO
其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。
更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。
<NSKeyValueObserving>
或者 KVO
是一个非正式的协议,该协议定义了一个观察和通知对象之间状态变化的通用机制。作为一个非正式的协议,你在使用该协议的类中看不到惯用的写法<NSKeyValueObserving>
,实际上,这个协议只是隐式地由NSObject
实现,继承NSObject
的子类默认能够使用这个协议。
放一部分NSKeyValueObserving.h对于NSObject
的拓展代码
@interface NSObject(NSKeyValueObserving)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;
@end
@interface NSObject(NSKeyValueObserverRegistration)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
从拓展名称就可以看出,使用KVO
需要注册监听器,也需要删除监听器。监听过程需要使用observeValueForKeyPath
回调方法。
所以使用方法就可以推测出个大概来:
- addObserver方法注册一个监听器
- 复写observeValueForKeyPath回调,获得监听到的信息,做对应操作。
- 使用结束removeObserver,这很重要。
实现
最后对上面代码做一些改动,我需要对Account对象的balance做监听,当balance内容改变,我要做输出处理。
#pragma mark - For KVO
- (void)setAccount:(Account *)account {
_account = account;
//add observer
[_account addObserver:self
forKeyPath:@"balance"
options:NSKeyValueObservingOptionNew
context:nil];
}
//override
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:@"balance"]) {
NSLog(@"keyPath = %@, object = %@, newValue = %.2f, context = %@", keyPath, object, [[change objectForKey:@"new"] floatValue], context);
}
}
- (void)dealloc {
[_account removeObserver:self forKeyPath:@"balance"];
}
//main
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person1 = [[Person alloc] init];
[person1 setValue:@"Wossoneri" forKey:@"name"];
[person1 setValue:@25 forKey:@"age"]; //私有变量也可以访问
[person1 setValue:@"male" forKey:@"sex"]; //私有变量也可以访问
[person1 showMessage];
Account *account1 = [[Account alloc] init];
person1.account = account1;
[person1 setValue:@1000.0 forKeyPath:@"account.balance"];
[person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];
//KVO
account1.balance = 4000.0;
NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);
}
// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
keyPath = balance, object = , newValue = 1000.00, context = (null)
set balance invoked
get balance invoked
keyPath = balance, object = , newValue = 4000.00, context = (null)
get balance invoked
Person1`s balance is : 4000.00
Person1`s salary is : 300.00
优点
当有属性改变,KVO
会提供自动的消息通知。这样的架构有很多好处。首先,开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。这是KVO
机制提供的最大的优点。因为这个方案已经被明确定义,获得框架级支持,可以方便地采用。开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可以在工程里使用。其次,KVO
的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值。
Swift的KVO与KVC
Swift版本的的就看这篇文章吧,内容很详细。
漫谈 KVC 与 KVO
Reference
iOS开发系列—Objective-C之KVC、KVO